﻿// CosignModule.cs
// 
// Copyright (C) 2011 The Pennslyvania State University
// 
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of
// the License, or (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful, but 
// WITHOUT ANY WARRANTY; without even the implied warranty 
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
// PURPOSE. See the GNU General Public License for more details. 
// You should have received a copy of the GNU General Public 
// License along with this program; if not, write to the Free 
// Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, 
// MA 02111-1307 USA

#region

using System;
using System.Security.Principal;
using System.Web;
using NLog;

#endregion

namespace CosignManaged
{
    public class CosignModule : IHttpModule
    {
        private static Logger logger = GlobalLogger.Instance.GetLogger("CosignModule");
        private static CosignConfiguration _cosignConfig;
        private static Boolean _cosignAvailable = true;

        /// <summary>
        ///   Initializes a module and prepares it to handle requests.
        /// </summary>
        /// <param name = "context">An <see cref = "T:System.Web.HttpApplication" /> that provides access to the methods, properties, and events common to all application objects within an ASP.NET application</param>
        public void Init(HttpApplication context)
        {
            logger.Trace("Cosign Module loaded");
            context.AuthenticateRequest += ContextAuthenticateRequest;
            context.EndRequest += ContextEndRequest;
        }

        /// <summary>
        ///   Handler for the authenticate request
        /// </summary>
        /// <param name = "sender">The sender.</param>
        /// <param name = "e">The <see cref = "System.EventArgs" /> instance containing the event data.</param>
        public void ContextAuthenticateRequest(object sender, EventArgs e)
        {
            logger.Trace("AuthenticateRequest started");
            var application = (HttpApplication) sender;
            HttpContext context = application.Context;

            try
            {
                logger.Trace("Loading configuration");
                _cosignConfig = new CosignConfiguration();
            }
            catch (Exception ex)
            {
                logger.FatalException("Cosign Configuration Invalid", ex);
                throw new HttpException(500, "Internal Server Error", ex);
            }

            //Check to see if we are doing basic auth or cosign
            HttpCookie basicAuthCookie = context.Request.Cookies[_cosignConfig.ServiceName + "-BasicAuth"];
            if (basicAuthCookie != null && basicAuthCookie.Value == "true" && _cosignConfig.FallBackBasicAuth)
            {
                //Basic auth has been set in previous request, cookie is there, so assume cosign not working at the moment
                //Force ssl for basic auth fallback
                if (!context.Request.IsSecureConnection)
                {
                    throw new HttpException(403, "SSL required for Basic Authentication");
                }
                if (!String.IsNullOrEmpty(context.Request.Headers["Authorization"]))
                {
                    if (!BasicAuthHelpers.AuthenticateUser(_cosignConfig.BasicAuthDomain))
                    {
                        DenyAccess(null);
                    }
                }
                else
                {
                    if (_cosignConfig.Protected == 1)
                    {
                        DenyAccess(null);
                    }
                }
            }
            else
            {
                //Not using basic auth fallback and no basic auth has been set so far, lets use cosign
                logger.Trace("Getting service cookie from request");
                HttpCookie serviceCookie = context.Request.Cookies[_cosignConfig.ServiceName];
                if (_cosignConfig.Protected != 0 && serviceCookie != null)
                {
                    logger.Trace("Site is set as protected/allowPublic and there is a service cookie");
                    String cookieFilePath = _cosignConfig.CookieDb + "\\" + serviceCookie.Value;
                    logger.Trace("Check cached cookie");
                    var cosignCookie = new CosignCookie(cookieFilePath, _cosignConfig.CookieTimeOut);
                    if (cosignCookie.ErrorCode != CosignGlobals.CosignLoggedIn)
                    {
                        logger.Trace("Cached cookie not found or not valid");

                        int retries = 0;
                        bool fatalErrorOrFound = false;
                        while (retries < _cosignConfig.ServerRetries && fatalErrorOrFound == false)
                        {
                            logger.Trace("Trying to validate cookie retry - {0}", retries);
                            try
                            {
                                cosignCookie = CosignConnection.ConnectAndValidate(serviceCookie.Value, true,
                                                                                   _cosignConfig);
                            }
                            catch (Exception ex)
                            {
                                logger.FatalException("Exception while validating the cookie", ex);
                                cosignCookie.ErrorCode = CosignGlobals.CosignError;
                            }

                            switch (cosignCookie.ErrorCode)
                            {
                                case CosignGlobals.CosignLoggedOut:
                                    logger.Trace("User is already logged out, redirecting to login");
                                    if (BasicAuthHelpers.CheckCosignServer(_cosignConfig.CosignServerUrl))
                                    {
                                        context.Response.Redirect(
                                            _cosignConfig.CosignServerUrl + _cosignConfig.ServiceName + "&" +
                                            context.Request.Url.AbsoluteUri, true);
                                    }
                                    else
                                    {
                                        fatalErrorOrFound = true;
                                    }
                                    break;
                                case CosignGlobals.CosignError:
                                case CosignGlobals.CosignLoggedIn:
                                    fatalErrorOrFound = true;
                                    break;
                            }
                            retries++;
                        }
                    }

                    if (cosignCookie.ErrorCode == CosignGlobals.CosignLoggedIn)
                    {
                        //Need to figure out how to deal with proxies ip addresses

                        //logger.Info("Client ip address, Forwarded Address - {0} , {1}", context.Request.UserHostAddress, context.Request.ServerVariables["X_FORWARDED_FOR"]);
                        //if (context.Request.UserHostAddress == cosignCookie.ClientIpAddress)
                        //{
                        context.User =
                            new GenericPrincipal(new GenericIdentity(cosignCookie.UserId, "Cosign"),
                                                 null);
                        application.Request.ServerVariables.Set("HTTP_COSIGN_FACTOR", cosignCookie.Factor);
                        application.Request.ServerVariables.Set("HTTP_REMOTE_USER", cosignCookie.UserId);
                        application.Request.ServerVariables.Set("HTTP_REMOTE_REALM", cosignCookie.Realm);
                        application.Request.ServerVariables.Set("HTTP_COSIGN_SERVICE",
                                                                _cosignConfig.ServiceName);
                        logger.Trace("Cosign cookie found and validated");
                        //}
                        //else
                        //{
                        //logger.Warn("Client ip address is different than the cookie - {0} , {1}, {2}", serviceCookie.Value, context.Request.UserHostAddress, context.Request.ServerVariables["REMOTE_ADDR"]);

                        //What do we do here, seems silly to do a server unavailable, redirect to cosign server and try again
                        //if (cosignConfig.Protected == 1)
                        //context.Response.Redirect(cosignConfig.CosignServerUrl + cosignConfig.ServiceName + "&" + context.Request.Url.AbsoluteUri,true);
                        //}
                    }
                    else if (_cosignConfig.Protected == 1)
                    {
                        //Retry means that cosign must be working otherwise would not get a retry
                        if (cosignCookie.ErrorCode == CosignGlobals.CosignRetry)
                        {
                            context.Response.Redirect(
                                _cosignConfig.CosignServerUrl + _cosignConfig.ServiceName + "&" +
                                context.Request.Url.AbsoluteUri, true);
                        }

                        //Check to see if we fall back to basic auth and do so if true
                        if (_cosignConfig.FallBackBasicAuth)
                        {
                            DenyAccess(_cosignConfig.ServiceName);
                        }
                        else
                        {
                            logger.Fatal("An Error has occured that prevents cosign doing anything further - {0}",
                                         cosignCookie.ErrorMessage);
                            ServiceUnavailable();
                        }
                    }
                }
                else if (_cosignConfig.Protected == 1 && serviceCookie == null)
                {
                    if (BasicAuthHelpers.CheckCosignServer(_cosignConfig.CosignServerUrl))
                    {
                        if (!String.IsNullOrEmpty(_cosignConfig.SiteEntryUrl))
                        {
                            logger.Trace("Service Cookie not found, redirecting to webaccess server - {0}, {1}, {2}",
                                         _cosignConfig.CosignServerUrl, _cosignConfig.ServiceName,
                                         _cosignConfig.SiteEntryUrl);
                            context.Response.Redirect(
                                _cosignConfig.CosignServerUrl + _cosignConfig.ServiceName + "&" +
                                _cosignConfig.SiteEntryUrl,
                                true);
                        }
                        else
                        {
                            logger.Trace("Service Cookie not found, redirecting to webaccess server - {0}, {1}, {2}",
                                         _cosignConfig.CosignServerUrl, _cosignConfig.ServiceName,
                                         context.Request.Url.AbsoluteUri);
                            context.Response.Redirect(
                                _cosignConfig.CosignServerUrl + _cosignConfig.ServiceName + "&" +
                                context.Request.Url.AbsoluteUri, true);
                        }
                    }
                    else
                    {
                        if (_cosignConfig.FallBackBasicAuth)
                        {
                            DenyAccess(_cosignConfig.ServiceName);
                        }
                        else
                        {
                            logger.Fatal("An Error has occured that prevents cosign doing anything further");
                            ServiceUnavailable();
                        }
                    }
                }
            }
        }

        /// <summary>
        ///   Returns a 401 access denied so to start the basic auth process
        /// </summary>
        private void DenyAccess(String serviceName)
        {
            _cosignAvailable = false;
            HttpContext context = HttpContext.Current;
            if (!String.IsNullOrEmpty(serviceName))
            {
                var cookie = new HttpCookie(serviceName + "-BasicAuth", "true");
                context.Response.Cookies.Set(cookie);
                logger.Trace("Cookie to determine whether to use Basic Auth has been set");
            }
            context.Response.StatusCode = 401;
            context.Response.End();
        }


        /// <summary>
        ///   Returns a 503 error that the service is unavailabe, should only get here if nothing else can be done
        /// </summary>
        private void ServiceUnavailable()
        {
            HttpContext context = HttpContext.Current;
            context.Response.StatusCode = 503;
            context.Response.StatusDescription = "Service Unavailable";
            context.Response.End();
        }

        /// <summary>
        ///   Disposes of the resources (other than memory) used by the module that implements <see cref = "T:System.Web.IHttpModule" />.
        /// </summary>
        public void Dispose()
        {
            logger.Trace("Module destroyed");
        }

        /// <summary>
        /// End request method that determines if we need to pass a 401 code and the WWW-Authtenticate header
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        public void ContextEndRequest(Object source, EventArgs e)
        {
            // check if module is enabled and cosign is not available
            if (_cosignConfig.Protected == 1 && !_cosignAvailable)
            {
                HttpContext context = HttpContext.Current;
                if (context.Response.StatusCode == 401)
                {
                    context.Response.StatusCode = 401;
                    context.Response.AddHeader("WWW-Authenticate",
                                               String.Format("Basic realm=\"{0}\"", _cosignConfig.BasicAuthRealm));
                }
            }
        }
    }
}